<template>
  <div class="sku-product-detail-page">
    <header class="page-header">
      <div class="title-wrap">
        <h2>产品明细</h2>
        <span class="sub">维护产品 SKU / BOM / 报价相关信息</span>
      </div>
      <div class="actions">
        <div class="search-box">
          <svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <circle cx="11" cy="11" r="8" />
            <path d="m21 21-4.35-4.35" />
          </svg>
          <input 
            v-model="keyword" 
            placeholder="搜索 产品编码" 
            @keyup.enter="forceSearch" 
            aria-label="产品编码搜索输入"
          />
          <span v-if="searchDebouncing" class="search-hint">搜索中...</span>
        </div>
        <select v-model="filterStatus" class="select">
          <option value="">状态(全部)</option>
          <option value="pending">待维护</option>
          <option value="completed">已完成</option>
        </select>
    <button class="btn primary elevate no-wrap" @click="openCreateDialog">
          <svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <line x1="12" y1="5" x2="12" y2="19" />
            <line x1="5" y1="12" x2="19" y2="12" />
          </svg>
          新增产品
        </button>
      </div>
    </header>

    <section v-if="filtered.length" class="table-wrapper card">
      <table class="entry-table fancy">
        <thead>
          <tr>
            <th style="width:140px">产品类型</th>
            <th style="width:140px">产品编码</th>
            <th style="width:120px">属性</th>
            <th style="width:120px">BOM</th>
            <th style="width:120px">报价表</th>
            <th style="width:110px">状态</th>
            <th style="width:150px">创建时间</th>
            <th style="width:150px">修改时间</th>
            <th style="width:100px">操作</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="p in filtered" :key="p.id">
            <td>{{ p.category }}</td>
            <td>{{ p.code }}</td>
            <td>
              <button class="btn view sm" @click="openAttrDialog(p)" title="查看属性"><span class="txt">查看</span></button>
            </td>
            <td>
              <button class="btn view sm" @click="openBomDialog(p)" title="查看BOM"><span class="txt">查看</span></button>
            </td>
            <td>
              <button class="btn view sm" @click="openQuotationDialog(p)" title="查看报价"><span class="txt">查看</span></button>
            </td>
            <td>
              <span class="status-badge" :class="p.status">{{ p.status==='pending' ? '待维护' : '已完成' }}</span>
            </td>
            <td>{{ formatDate(p.createdAt) }}</td>
            <td>{{ formatDate(p.updatedAt) }}</td>
            <td class="ops">
              <button class="btn ghost sm blue" @click="edit(p)" :disabled="p.status==='completed'" :title="p.status==='completed' ? '已完成不可编辑' : '编辑'">编辑</button>
            </td>
          </tr>
        </tbody>
      </table>
    </section>
    <p v-else class="empty-tip" v-if="!loadingList && !loadError">暂无数据</p>
    <p v-if="loadError" class="empty-tip" style="color:#dc2626">{{ loadError }}</p>
    <div v-if="loadingList" class="empty-tip">加载中...</div>
    <div class="pagination-bar" v-if="total>0 && !loadingList">
      <button class="btn ghost sm" :disabled="currentPage===1" @click="loadList(currentPage-1)">上一页</button>
      <span class="pager-info">第 {{ currentPage }} / {{ totalPages }} 页 (共 {{ total }} 条)</span>
      <button class="btn ghost sm" :disabled="currentPage===totalPages" @click="loadList(currentPage+1)">下一页</button>
    </div>

    <!-- 编辑 / 新建 产品基础信息 + BOM & 非物料成本 编辑对话框 -->
  <dialog ref="dialogRef" class="entry-dialog redesigned large" @close="onDialogNativeClose" @cancel.prevent="onDialogCancel" @keydown="onDialogKeydown">
      <form method="dialog" class="dialog-shell" @submit.prevent="saveAll" aria-labelledby="prodDialogTitle">
        <div class="dialog-header">
          <h3 id="prodDialogTitle" class="dialog-title">{{ current ? '编辑产品 BOM & 成本' : '新增产品' }}</h3>
            <button type="button" class="ui-close-btn" @click="closeDialog" aria-label="关闭">
              <svg class="icon-x" viewBox="0 0 24 24" fill="none"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
            </button>
        </div>
        <div class="dialog-content-scroll big-grid">
          <div class="left-stack">
          <section class="form-section-card base-info">
            <header class="section-title-row">
              <h4 class="section-title">基础信息（只读）</h4>
            </header>
            <div class="grid-two">
              <label class="field-block">产品类型
                <input v-model="form.category" required disabled class="readonly" />
              </label>
              <label class="field-block">产品编码
                <input v-model="form.code" required disabled class="readonly" />
              </label>
              <label class="field-block">产品名称
                <!-- 使用可换行的只读展示块，避免长名称截断；保持与输入框风格一致 -->
                <div class="readonly multiline-name" :title="form.name">{{ form.name || '—' }}</div>
              </label>
            </div>
          </section>
          <section class="form-section-card extra-fees-edit">
            <header class="section-title-row">
              <h4 class="section-title">非物料成本</h4>
            </header>
            <div class="grid-three">
              <label class="field-block">港杂费
                <input
                  class="fee-input"
                  type="number"
                  :value="extraFees.harbor === '' ? '' : extraFees.harbor"
                  @input="onFeeInput('harbor', $event)"
                  @blur="onFeeBlur('harbor')"
                  min="0"
                  step="0.01"
                />
              </label>
              <label class="field-block">裁切费用
                <input
                  class="fee-input"
                  type="number"
                  :value="extraFees.cutting === '' ? '' : extraFees.cutting"
                  @input="onFeeInput('cutting', $event)"
                  @blur="onFeeBlur('cutting')"
                  min="0"
                  step="0.01"
                />
              </label>
              <label class="field-block">人工制造费用
                <input
                  class="fee-input"
                  type="number"
                  :value="extraFees.labor === '' ? '' : extraFees.labor"
                  @input="onFeeInput('labor', $event)"
                  @blur="onFeeBlur('labor')"
                  min="0"
                  step="0.01"
                />
              </label>
            </div>
            <div class="fee-total-line">小计：<strong>{{ feeTotal.toFixed(2) }}</strong></div>
          </section>
          </div>
          
          <!-- 右侧栈：BOM + 包装方式 -->
          <div class="right-stack">
          <section class="form-section-card bom-edit">
            <header class="section-title-row bom-head-inline">
              <h4 class="section-title">BOM 行</h4>
              <div class="inline-tools">
                <input v-model="bomFilter" placeholder="筛选 物料编码/名称" class="filter-inline" />
                <button type="button" class="btn secondary sm" @click="openAddLine">新增行</button>
                <button type="button" class="btn secondary sm" @click="openImportLines">导入行</button>
              </div>
            </header>
            <div class="bom-table-wrapper">
              <table class="entry-table fancy">
                <thead>
                  <tr>
                    <th style="width: 60px">类型</th><th style="width: 80px">编码</th><th style="width: 140px">名称</th><th style="width: 120px">物料分类</th><th style="width: 60px">规格</th><th style="width: 80px">费用模式</th><th style="width: 100px">金额/单价(¥)</th><th style="width: 80px">数量</th><th style="width: 100px">行成本(¥)</th><th style="width: 70px">操作</th>
                  </tr>
                </thead>
                <tbody>
                  <tr v-for="(r,idx) in filteredBom" :key="r.id || r._localId || idx">
                    <td class="type-cell">{{ r.type==='production'?"物料":"包装" }}</td>
                    <td>{{ r.materialCode || '-' }}</td>
                    <td class="strong">{{ r.costName || r.materialName }}</td>
                    <td class="category-cell">{{ r.category || '-' }}</td>
                    <td class="spec-cell" :title="r.spec">{{ r.spec || '-' }}</td>
                    <td class="fee-mode">{{ r.feeDetail ? (r.feeDetail.mode==='fixed'?"固定":"计数") : '-' }}</td>
                    <td class="fee-amount">{{ r.feeDetail ? r.feeDetail.amount.toFixed(2) : (r.unitCost||0).toFixed(2) }}</td>
                    <td class="qty">
                      <template v-if="r.feeDetail && r.feeDetail.mode==='count'">
                        <input type="number" class="qty-input" v-model.number="r.feeDetail.quantity" min="1" step="1" @blur="ensureQty(r)" />
                      </template>
                      <template v-else>-</template>
                    </td>
                    <td class="line-cost">{{ lineTotal(r).toFixed(2) }}</td>
                    <td class="actions">
                      <button type="button" class="link-btn" @click="editLine(r)">编辑</button>
                      <button type="button" class="link-btn danger" @click="requestDeleteLine(r)">删除</button>
                    </td>
                  </tr>
                  <tr v-if="!filteredBom.length"><td colspan="10" class="empty">暂无数据</td></tr>
                </tbody>
                <tfoot>
                  <tr><td colspan="8" class="tar">合计</td><td class="line-cost">{{ bomTotal.toFixed(2) }}</td><td></td></tr>
                </tfoot>
              </table>
            </div>
          </section>
          </div>

            <!-- 完成确认自定义弹层 -->
            <transition name="fade-scale">
              <div v-if="dialogOpen && completeConfirming" class="confirm-center-layer" role="alertdialog" aria-modal="true" aria-labelledby="completeTitle">
                <div class="confirm-card center">
                  <header class="confirm-head">
                    <svg viewBox="0 0 24 24" class="warn-icon" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v4"/><path d="M12 18v4"/><path d="M4 12h4"/><path d="M16 12h4"/><circle cx="12" cy="12" r="4"/></svg>
                    <h5 id="completeTitle">完成确认</h5>
                  </header>
                  <p class="confirm-text">确认该产品已维护完成？<strong>完成后将不可再编辑 BOM 与非物料成本。</strong></p>
                  <div class="confirm-actions">
                    <button type="button" class="btn secondary sm" @click="cancelCompleteConfirm" :disabled="completing">取消</button>
                    <button type="button" class="btn ghost sm" style="color:#16a34a; border-color:#16a34a" @click="confirmComplete" :disabled="completing">{{ completing ? '完成中...' : '确认完成' }}</button>
                  </div>
                </div>
              </div>
            </transition>
            <!-- 自定义删除确认面板 -->
            <transition name="fade-scale">
              <div v-if="dialogOpen && deletingLine" class="confirm-center-layer" role="alertdialog" aria-modal="true" aria-labelledby="delTitle">
                <div class="confirm-card center">
                  <header class="confirm-head">
                    <svg viewBox="0 0 24 24" class="warn-icon" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 9v4"/><circle cx="12" cy="17" r="1"/><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z"/></svg>
                    <h5 id="delTitle">删除确认</h5>
                  </header>
                  <p class="confirm-text">确定删除该 BOM 行 <strong>{{ deletingLine.costName || deletingLine.materialName || deletingLine.materialCode }}</strong> 吗？此操作不可撤销。</p>
                  <div class="confirm-actions">
                    <button type="button" class="btn secondary sm" @click="cancelDelete">取消</button>
                    <button type="button" class="btn ghost danger sm" @click="confirmDelete">删除</button>
                  </div>
                </div>
              </div>
            </transition>
        </div>
        <div class="dialog-actions sticky-actions no-hint">
          <div class="actions-btns full">
            <button class="btn secondary" value="cancel" type="button" @click="closeDialog">取消</button>
            <button class="btn primary" type="submit" :disabled="current?.status==='completed'">保存修改</button>
            <button v-if="current && current.status==='pending'" type="button" class="btn success" @click="requestComplete">完成</button>
          </div>
        </div>
      </form>
  <!-- 复用简化 BOM 条目新增/编辑组件 -->
  <BomSimpleEntryDialog
    ref="bomSimpleDialogRef"
    @save="onSaveBomSimple"
    @request-attr-view="onBomAttrView"
    @request-attr-edit="onBomAttrEdit"
    :duplicate-graceful="true"
  />
  <!-- 动态属性设置弹窗复用 -->
  <DynamicAttributeDialog ref="dynAttrDialogRef" />
    </dialog>

    <!-- 属性 / BOM / 报价 的三个弹窗组件 -->
    <AttributeDialog ref="attrDialogRef" />
  <BomViewDialog ref="bomDialogRef" />
  <QuotationDialog ref="quotationDialogRef" />
  <ProductCreateDialog ref="productCreateDialogRef" @added="onProductAdded" />
  <BomLineImportDialog ref="bomLineImportDialogRef" @import="onImportBomLines" />
  <!-- 行属性查看弹窗改为复用 AttributeViewer，高一致性展示 -->
  <AttributeViewer ref="bomAttrViewerRef" />
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { useRoute } from 'vue-router';
import BomSimpleEntryDialog from '../bomSimple/components/BomSimpleEntryDialog.vue';
import DynamicAttributeDialog from '../bomSimple/components/DynamicAttributeDialog.vue';
import AttributeDialog from './components/AttributeDialog.vue';
import BomViewDialog from '../bomSimple/components/BomViewDialog.vue';
import QuotationDialog from './components/QuotationDialog.vue';
import ProductCreateDialog from './components/ProductCreateDialog.vue';
import BomLineImportDialog from './components/BomLineImportDialog.vue';
import AttributeViewer from '../bomSimple/components/AttributeViewer.vue';

import { useSkuStore, SkuProductBase } from '../../store/sku';
import { editBom, buildBomEditBody } from '../../services/bom';
import { fetchSkuList, fetchSkuEncodingDetail, addSku, editSku } from '../../services/sku';

interface SkuProductRow extends SkuProductBase { id?: string }

const keyword = ref(''); // 输入中的即时值
const searchKeyword = ref(''); // 防抖后用于真正发请求的值
let searchDebounceTimer: number | null = null;
const SEARCH_DEBOUNCE_DELAY = 500;
const searchDebouncing = ref(false);
const filterStatus = ref('');
const route = useRoute();

const skuStore = useSkuStore();
// 远程分页数据
const records = ref<SkuProductRow[]>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(10);
const totalPages = computed(()=> Math.max(1, Math.ceil(total.value / pageSize.value)));
const loadingList = ref(false);
const loadError = ref('');

async function loadList(page?: number){
  if(page) currentPage.value = page;
  loadingList.value = true; loadError.value='';
  try {
    const prod = (route.query.prod as string) || '';
  const kw = searchKeyword.value.trim();
    const params = { page: currentPage.value, pageSize: pageSize.value, invCode: kw, invName: kw, productType: prod, status: filterStatus.value } as any;
    const data = await fetchSkuList(params);
    total.value = data.total; currentPage.value = data.current; pageSize.value = data.size;
  records.value = (data.records||[]).map((r: any) => {
      const exists = skuStore.state.list.find(p=> p.code===r.invCode);
      const mappedStatus = (r.status==='已完成' || r.status==='completed') ? 'completed':'pending';
      if(!exists){
        skuStore.state.list.push({
          code: r.invCode,
          category: r.productType,
          name: r.name,
          status: mappedStatus,
          createdAt: Date.now(),
          updatedAt: Date.now(),
          attributes: {}
        } as any);
      }
      // 同步远端 bomS -> 本地 store（仅在本地尚未有或为空时覆盖）
      if(Array.isArray(r.bomS) && r.bomS.length){
        const transformed = r.bomS.map((bItem: any, idx: number) => {
          const b = bItem.bom || {};
          const chineseType = b.bomType;
          const internalType = (chineseType === '包装部件' || chineseType === 'package') ? 'packaging' : 'production';
          const feeMode = b.feeModel === 'count' ? 'count' : 'fixed';
          const quantity = feeMode === 'count' ? (bItem.quantity || 1) : (bItem.quantity || 1); // 计数模式数量来自 bItem.quantity
          // 属性反写
          const attrValues: Record<string, any> = {};
          (b.attributes || []).forEach((a: any) => { attrValues[a.propertyName] = a.propertyValue; });
          return {
            id: b.id || '',
            _localId: `remote-${b.id || Date.now()}-${idx}`,
            type: internalType,
            materialCode: b.invCode || '',
            materialName: b.name || '',
            costName: b.name || '',
            materialCategory: b.className || '',
            category: b.className || '',
            classId: b.classId,
            qty: quantity,
            unit: 'pcs',
            lossFactor: 1,
            spec: '',
            attributes: attrValues,
            attributeDefinitions: (b.attributes || []).map((a: any, i: number) => ({ code: a.propertyName, name: a.propertyName, control: 'TextBox', sort: a.sort || i+1 })),
            attributeValues: attrValues,
            feeDetail: feeMode === 'fixed'
              ? { mode: 'fixed', amount: b.feeAmount || 0, currency: 'CNY' }
              : { mode: 'count', amount: b.feeAmount || 0, quantity: quantity, currency: 'CNY' }
          };
        });
        // 始终以远端数据覆盖，确保与后端同步（编辑单行后刷新）
        skuStore.state.bomMap[r.invCode] = transformed;
      }
      // 同步非物料成本（港杂费、裁切费、人工费）到 store
      skuStore.setExtraFees(r.invCode, { harbor: r.port || 0, cutting: r.cutting || 0, labor: r.labor || 0 });
      return {
        id: r.id, // 后端真实 sku id
        code: r.invCode,
        category: r.productType,
        name: r.name,
        status: mappedStatus,
        createdAt: exists?.createdAt || Date.now(),
        updatedAt: exists?.updatedAt || Date.now(),
        attributes: exists?.attributes || {}
      } as SkuProductRow;
    });
  } catch(e:any){ loadError.value = e.message || '加载失败'; }
  finally { loadingList.value = false; }
}
const filtered = computed(()=> records.value);

// 防抖监听：仅在搜索词防抖稳定后发起查询
watch(searchKeyword, () => { loadList(1); });
// 其他筛选状态立即触发查询（保持快速响应）
watch([filterStatus, () => route.query.prod], () => { loadList(1); });

// 输入过程中更新 searchKeyword（防抖）
watch(keyword, (val) => {
  if (searchDebounceTimer !== null) {
    clearTimeout(searchDebounceTimer);
  }
  searchDebouncing.value = true;
  searchDebounceTimer = window.setTimeout(() => {
    searchKeyword.value = val;
    searchDebouncing.value = false;
    searchDebounceTimer = null;
  }, SEARCH_DEBOUNCE_DELAY);
});

function forceSearch(){
  if (searchDebounceTimer !== null) {
    clearTimeout(searchDebounceTimer);
    searchDebounceTimer = null;
  }
  searchKeyword.value = keyword.value;
  searchDebouncing.value = false;
}

// 初次加载
loadList(1);

const dialogRef = ref<HTMLDialogElement | null>(null);
const dialogOpen = ref(false);
const current = ref<SkuProductRow | null>(null);

const form = ref({ category: '', code: '', name: '', status: 'pending' as 'pending' | 'completed' });
// 允许输入阶段为 ''，避免清空时产生 NaN 导致视图闪烁
interface ExtraFees { harbor: number | ''; cutting: number | ''; labor: number | ''; }
const extraFees = ref<ExtraFees>({ harbor:0, cutting:0, labor:0 });
const bomWorking = ref([] as any[]);
const bomFilter = ref('');
const canSubmit = computed(() => form.value.category && form.value.code);
const saving = ref(false); // 保存按钮loading禁用状态

function openCreate() { /* 已弃用：使用独立新增页面 */ }
// 新增产品弹窗
const productCreateDialogRef = ref<InstanceType<typeof ProductCreateDialog> | null>(null);
function openCreateDialog(){ productCreateDialogRef.value?.open(); }
// 新增产品后：直接打开主编辑弹窗（以“新增”模式，无 current）并预填基础信息
function onProductAdded(payload:any){
  // 新增产品暂不进入 records 列表，也不写入 store，直到保存成功后再刷新列表
  const p = payload.product;
  const bomLines = payload.bomLines || [];
  current.value = null; // 表示新增模式
  form.value = { category: p.category, code: p.code, name: p.name || '', status: 'pending' };
  extraFees.value = { harbor:0, cutting:0, labor:0 };
  bomWorking.value = [...bomLines]; // 使用临时 BOM 行
  deletingLine.value = null;
  completeConfirming.value = false;
  const el = dialogRef.value; if (el) { el.classList.remove('closing'); el.showModal(); dialogOpen.value = true; }
}
function edit(row: SkuProductRow) {
  current.value = row;
  form.value = { category: row.category, code: row.code, name: row.name || '', status: row.status };
  // 加载 BOM & 非物料成本
  bomWorking.value = [...skuStore.getBom(row.code)];
  extraFees.value = { ...skuStore.getExtraFees(row.code) };
  deletingLine.value = null; // 重置删除确认状态
  const el = dialogRef.value; if (el) { el.classList.remove('closing'); el.showModal(); dialogOpen.value = true; }
}
function closeDialog() {
  const el = dialogRef.value; if (!el) return; if (el.classList.contains('closing')) return; el.classList.add('closing');
  const done = () => { el.removeEventListener('transitionend', done); el.classList.remove('closing'); el.close(); };
  // 统一与 BOM 编辑弹窗的退出动画节奏：若没有 .entry-dialog.closing 样式则快速关闭
  el.addEventListener('transitionend', done);
  // 缩短兜底超时时间，避免 450ms 空等待（当前无关闭过渡样式）
  setTimeout(done, 180);
  deletingLine.value = null; // 关闭时清理
  dialogOpen.value = false;
}
async function saveAll() {
  if(saving.value) return;
  // 时间格式化工具（与后端示例保持一致）
  const formatDateTime = (ts: number) => {
    const d = new Date(ts); const pad = (n: number) => String(n).padStart(2,'0');
    return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
  };
  // 构建后端需要的 BOM body 数据
  const bomS = bomWorking.value.map((r:any) => {
    // 严格判定框条：仅依据后端提供的 classId === 65，不再用名称模糊匹配，避免误判
    const isFrame = Number(r.classId) === 65;
    const feeModel: 'fixed' | 'count' = r.feeDetail ? r.feeDetail.mode : 'fixed';
    const feeAmount = r.feeDetail ? r.feeDetail.amount : (r.unitCost||0);
    // 生成 attributes 数组：优先使用 attributeDefinitions 排序；否则使用 attributes 对象键
    let attributes: { propertyName: string; propertyValue: string | number; sort: number }[] = [];
    if(r.attributeDefinitions && Array.isArray(r.attributeDefinitions) && r.attributeDefinitions.length){
      attributes = r.attributeDefinitions.map((def: any, idx: number) => {
        // 修复：需要正确处理属性值的键映射
        // def.code 是属性定义的代码（如 p_123），def.name 是属性名
        // attributeValues 中可能使用 def.code 作为键，也可能直接使用属性名
        const propCode = def.code || def.name;
        let propValue: any = '';
        if(r.attributeValues) {
          // 优先使用 code 查找，再尝试用 name
          propValue = r.attributeValues[propCode] ?? r.attributeValues[def.name] ?? '';
        } else if(r.attributes) {
          propValue = r.attributes[propCode] ?? r.attributes[def.name] ?? '';
        }
        return {
          propertyName: def.name || def.code,
          propertyValue: propValue,
          sort: def.sort || def.sortId || (idx+1)*10
        };
      });
    } else if(r.attributes){
      attributes = Object.keys(r.attributes).map((k, idx) => ({
        propertyName: k,
        propertyValue: r.attributes[k],
        sort: (idx+1)*10
      }));
    }
    const nowStr = formatDateTime(Date.now());
    return {
      bom: {
        // 后端字段 bomType 需传递编码值：product | package（不再使用中文）
        bomType: r.type === 'packaging' ? 'package' : 'product',
        classId: isFrame ? 65 : (Number(r.classId) || 0),
        className: isFrame ? 'PE框条' : (r.className || r.materialCategory || r.category || ''),
        id: r.id !== undefined ? r.id : '',
        invCode: r.materialCode || '',
        name: r.costName || r.materialName || '',
        // 框条行归属产品类型统一为 "通用"，其余使用当前产品类型
        productType: isFrame ? '通用' : form.value.category,
        attributes,
        feeModel,
        feeAmount,
        createTime: r.createTime || nowStr,
        updateTime: nowStr
      },
      quantity: feeModel === 'count' ? (r.feeDetail?.quantity || r.qty || 1) : (r.qty || 1)
    };
  });
  const normalizeFee = (v: number | '') => (typeof v === 'number' && isFinite(v)) ? v : 0;
  const body = {
    invCode: form.value.code,
    name: form.value.name || form.value.code,
    productType: form.value.category,
    status: current.value?.status || 'pending',
    port: normalizeFee(extraFees.value.harbor),
    cutting: normalizeFee(extraFees.value.cutting),
    labor: normalizeFee(extraFees.value.labor),
    bomS
  };
  try {
    saving.value = true;
    if(!current.value){
      // 新增
      const res = await addSku(body);
      const returnedId = res?.id; // 假设后端 envelope.data.id
      const now = Date.now();
      const row: SkuProductRow = { id: returnedId, code: form.value.code, category: form.value.category, name: form.value.code, status: 'pending', createdAt: now, updatedAt: now, attributes: {} };
      records.value.unshift(row);
      skuStore.state.list.push(row as any);
      skuStore.state.bomMap[row.code] = [...bomWorking.value];
      skuStore.setExtraFees(row.code, {
        harbor: normalizeFee(extraFees.value.harbor),
        cutting: normalizeFee(extraFees.value.cutting),
        labor: normalizeFee(extraFees.value.labor)
      });
      current.value = row;
    } else {
      // 编辑
      if(!current.value.id){ throw new Error('缺少 sku id，无法编辑'); }
      const editBody = { ...body, id: current.value.id };
      await editSku(editBody);
      current.value.updatedAt = Date.now();
      skuStore.state.bomMap[current.value.code] = [...bomWorking.value];
      skuStore.setExtraFees(current.value.code, {
        harbor: normalizeFee(extraFees.value.harbor),
        cutting: normalizeFee(extraFees.value.cutting),
        labor: normalizeFee(extraFees.value.labor)
      });
    }
    // 成功后刷新列表数据
    await loadList(currentPage.value);
    closeDialog();
  } catch(e:any){
    // 此处可增加错误提示 UI，比如单独的 error ref
    console.error('保存失败', e);
    alert(e.message || '保存失败');
  } finally { saving.value = false; }
}
// 完成确认逻辑（自定义弹层）
const completeConfirming = ref(false);
const completing = ref(false); // 完成操作的 loading 状态
function requestComplete(){ if(!current.value) return; completeConfirming.value = true; }
function cancelCompleteConfirm(){ completeConfirming.value = false; }
async function confirmComplete(){
  if(!current.value || completing.value) return;
  try {
    completing.value = true;
    
    // 时间格式化工具（与后端示例保持一致）
    const formatDateTime = (ts: number) => {
      const d = new Date(ts); const pad = (n: number) => String(n).padStart(2,'0');
      return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
    };
    
    // 构建后端需要的 BOM body 数据（与 saveAll 中的逻辑一致）
    const bomS = bomWorking.value.map((r:any) => {
      // 严格判定框条：仅依据后端提供的 classId === 65，不再用名称模糊匹配，避免误判
      const isFrame = Number(r.classId) === 65;
      const feeModel: 'fixed' | 'count' = r.feeDetail ? r.feeDetail.mode : 'fixed';
      const feeAmount = r.feeDetail ? r.feeDetail.amount : (r.unitCost||0);
      // 生成 attributes 数组：优先使用 attributeDefinitions 排序；否则使用 attributes 对象键
      let attributes: { propertyName: string; propertyValue: string | number; sort: number }[] = [];
      if(r.attributeDefinitions && Array.isArray(r.attributeDefinitions) && r.attributeDefinitions.length){
        attributes = r.attributeDefinitions.map((def: any, idx: number) => {
          // 修复：需要正确处理属性值的键映射
          // def.code 是属性定义的代码（如 p_123），def.name 是属性名
          // attributeValues 中可能使用 def.code 作为键，也可能直接使用属性名
          const propCode = def.code || def.name;
          let propValue: any = '';
          if(r.attributeValues) {
            // 优先使用 code 查找，再尝试用 name
            propValue = r.attributeValues[propCode] ?? r.attributeValues[def.name] ?? '';
          } else if(r.attributes) {
            propValue = r.attributes[propCode] ?? r.attributes[def.name] ?? '';
          }
          return {
            propertyName: def.name || def.code,
            propertyValue: propValue,
            sort: def.sort || def.sortId || (idx+1)*10
          };
        });
      } else if(r.attributes){
        attributes = Object.keys(r.attributes).map((k, idx) => ({
          propertyName: k,
          propertyValue: r.attributes[k],
          sort: (idx+1)*10
        }));
      }
      const nowStr = formatDateTime(Date.now());
      return {
        bom: {
          // 后端字段 bomType 需传递编码值：product | package（不再使用中文）
          bomType: r.type === 'packaging' ? 'package' : 'product',
          classId: isFrame ? 65 : (Number(r.classId) || 0),
          className: isFrame ? 'PE框条' : (r.className || r.materialCategory || r.category || ''),
          id: r.id !== undefined ? r.id : '',
          invCode: r.materialCode || '',
          name: r.costName || r.materialName || '',
          // 框条行归属产品类型统一为 "通用"，其余使用当前产品类型
          productType: isFrame ? '通用' : form.value.category,
          attributes,
          feeModel,
          feeAmount,
          createTime: r.createTime || nowStr,
          updateTime: nowStr
        },
        quantity: feeModel === 'count' ? (r.feeDetail?.quantity || r.qty || 1) : (r.qty || 1)
      };
    });
    const normalizeFee = (v: number | '') => (typeof v === 'number' && isFinite(v)) ? v : 0;
    
    // 构建完成请求的 body（状态标记为 completed）
    const body = {
      invCode: form.value.code,
      name: form.value.name || form.value.code,
      productType: form.value.category,
      status: 'completed', // 标记为完成
      port: normalizeFee(extraFees.value.harbor),
      cutting: normalizeFee(extraFees.value.cutting),
      labor: normalizeFee(extraFees.value.labor),
      bomS,
      id: current.value.id // 编辑时需要带上 id
    };
    
    // 调用 /api/sku/edit 接口
    await editSku(body);
    
    // 本地同步状态
    current.value.status = 'completed';
    current.value.updatedAt = Date.now();
    skuStore.state.bomMap[current.value.code] = [...bomWorking.value];
    skuStore.setExtraFees(current.value.code, {
      harbor: normalizeFee(extraFees.value.harbor),
      cutting: normalizeFee(extraFees.value.cutting),
      labor: normalizeFee(extraFees.value.labor)
    });
    
    completeConfirming.value = false;
    
    // 关闭弹窗并刷新列表
    closeDialog();
    await loadList(currentPage.value);
  } catch(e:any){
    console.error('完成失败', e);
    alert(e.message || '完成失败');
  } finally {
    completing.value = false;
  }
}

// BOM 编辑逻辑
function lineTotal(r: any){
  if(r.feeDetail){
    if(r.feeDetail.mode==='fixed') return r.feeDetail.amount;
    const qty = r.feeDetail.quantity ?? r.qty;
    return (r.feeDetail.amount||0) * qty;
  }
  return (r.unitCost||0) * r.qty * (r.lossFactor||1);
}
const filteredBom = computed(()=> {
  if(!bomFilter.value.trim()) return bomWorking.value;
  const t = bomFilter.value.trim().toLowerCase();
  return bomWorking.value.filter((i:any)=> [i.materialCode, i.materialName, i.costName].some(v=> v && v.toLowerCase().includes(t)));
});
const bomTotal = computed(()=> filteredBom.value.reduce((s:any,r:any)=> s+ lineTotal(r),0));
// 非物料成本汇总（空字符串视为 0）
const feeTotal = computed(() => {
  const v = extraFees.value;
  const toNum = (n: number | '') => (typeof n === 'number' && isFinite(n)) ? n : 0;
  return toNum(v.harbor) + toNum(v.cutting) + toNum(v.labor);
});
function onFeeInput(field: 'harbor'|'cutting'|'labor', e: Event){
  const target = e.target as HTMLInputElement;
  const raw = target.value.trim();
  if(raw === ''){ extraFees.value[field] = ''; return; }
  const normalized = raw.replace(',', '.');
  const num = Number(normalized);
  if(!isNaN(num) && num >= 0){
    extraFees.value[field] = Math.round(num * 100) / 100; // 两位小数
  } else {
    target.value = extraFees.value[field] === '' ? '' : String(extraFees.value[field]);
  }
}
function onFeeBlur(field: 'harbor'|'cutting'|'labor'){
  if(extraFees.value[field] === '') extraFees.value[field] = 0;
}
// 使用复用的简化 BOM 条目对话框
const bomSimpleDialogRef = ref<InstanceType<typeof BomSimpleEntryDialog>|null>(null);
function openAddLine(){ bomSimpleDialogRef.value?.open(); }
const bomLineImportDialogRef = ref<InstanceType<typeof BomLineImportDialog>|null>(null);
function openImportLines(){ bomLineImportDialogRef.value?.open(form.value.category); }
function editLine(row:any){
  // 映射现有行结构到 BomSimpleEntryDialog 期望的字段
  let attributeDefinitions = row.attributeDefinitions;
  let attributeValues = row.attributeValues;
  if((!attributeDefinitions || !attributeDefinitions.length) && row.attributes){
    attributeDefinitions = Object.keys(row.attributes).map((k,i)=> ({ code:k, name:k, control:'TextBox', sort:i+1 }));
    attributeValues = { ...row.attributes };
  }
  const mapped = {
    id: row.id,
    classId: row.classId, // 传入后端真实分类 Id，确保编辑保存时能正确提交
    costName: row.costName || row.materialName,
    bomType: row.type==='packaging' ? 'package' : 'product',
    productType: current.value?.category || '通用',
    materialCode: row.materialCode,
    materialCategory: row.materialCategory || row.category || 'PE框条',
    costMode: row.feeDetail?.mode || (row.unitCost ? 'count':'fixed'),
    fixedAmount: row.feeDetail?.mode==='fixed' ? row.feeDetail.amount : undefined,
    unitPrice: row.feeDetail?.mode==='count' ? row.feeDetail.amount : undefined,
    attributeSpec: row.spec,
    attributeDefinitions,
    attributeValues
  };
  bomSimpleDialogRef.value?.open(mapped as any);
}
// 自定义删除确认逻辑
const deletingLine = ref<any|null>(null);
function requestDeleteLine(row:any){ deletingLine.value = row; }
function cancelDelete(){ deletingLine.value = null; }
function confirmDelete(){
  if(!deletingLine.value) return;
  const targetId = deletingLine.value.id;
  bomWorking.value = bomWorking.value.filter(r=> r.id!==targetId);
  deletingLine.value = null;
  reseq();
}
async function onSaveBomSimple(payload:any){
  // 需求：行编辑保存后不立即刷新 /api/sku/search，仅在整体“保存修改”(saveAll) 时刷新。
  if(!payload) return;
  // 改进判定：即使缺少 id（自动封装生成的本地行），若列表中存在同名 + 分类的行视为编辑，避免重复插入
  let existingIdx = -1;
  if(payload.id){
    existingIdx = bomWorking.value.findIndex(r => r.id === payload.id);
  } else {
    existingIdx = bomWorking.value.findIndex(r => r.costName === payload.costName && r.materialCategory === payload.materialCategory);
  }
  const isEdit = existingIdx >= 0;
  try {
    if(isEdit){
      // 若拥有后端 id 才调用编辑接口；本地生成但未持久化的行无法调用 /api/bom/edit
      if(payload.id){
        try {
          const editBody = buildBomEditBody(payload);
          await editBom(editBody as any);
        } catch(e){ console.error('调用 /api/bom/edit 失败', e); }
      }
      // 本地同步该行数据
      const target = bomWorking.value[existingIdx];
      bomWorking.value[existingIdx] = {
        ...target,
        costName: payload.costName,
        materialCode: payload.materialCode,
        materialName: payload.costName,
        materialCategory: payload.materialCategory,
        classId: payload.classId ?? target.classId,
        spec: payload.attributeSpec,
        attributeDefinitions: payload.attributeDefinitions,
        attributeValues: payload.attributeValues,
        feeDetail: payload.costMode === 'fixed'
          ? { mode:'fixed', amount: payload.fixedAmount ?? 0, currency:'CNY' }
          : { mode:'count', amount: payload.unitPrice ?? 0, quantity: target.feeDetail?.quantity || 1, currency:'CNY' }
      };
    } else {
      // 新增：弹窗内部已调用 /api/bom/add，这里插入本地显示（不刷新远程列表）
      // 重要：确保 classId 和 className 被正确保留，用于后续保存时的正确映射
      const normalized = {
        id: payload.id,
        type: payload.bomType==='package' ? 'packaging':'production',
        materialCode: payload.materialCode,
        materialName: payload.costName,
        costName: payload.costName,
        materialCategory: payload.materialCategory,
        category: payload.materialCategory, // 双重保留：category 用于显示，materialCategory 用于查询
        className: payload.materialCategory, // 保留分类名称供后端提交
        classId: payload.classId, // 保留后端分类 ID
        qty: 1,
        unit: 'pcs',
        lossFactor: 1,
        spec: payload.attributeSpec,
        attributeDefinitions: Array.isArray(payload.attributeDefinitions) ? [...payload.attributeDefinitions] : [],
        attributeValues: payload.attributeValues ? { ...payload.attributeValues } : {},
        feeDetail: payload.costMode==='fixed'
          ? { mode:'fixed', amount: payload.fixedAmount ?? payload.unitPrice ?? 0, currency:'CNY' }
          : { mode:'count', amount: payload.unitPrice ?? payload.fixedAmount ?? 0, quantity: 1, currency:'CNY' }
      };
      // 重复名称标记：如果后端判定重复而我们仍收到 __duplicateName，则不重复插入
      if(payload.__duplicateName){
        const exist = bomWorking.value.find(r => r.costName === normalized.costName);
        if(!exist) bomWorking.value.push(normalized);
      } else {
        bomWorking.value.push(normalized);
      }
    }
  } catch(err){
    console.error('产品页 BOM 行保存失败', err);
  } finally {
    reseq(); // 仅本地重新排序，不触发远端刷新
  }
}
// 导入 BOM 行合并
function onImportBomLines(lines:any[]){
  if(!Array.isArray(lines) || !lines.length) return;
  const existingNameSet = new Set(bomWorking.value.map(l => l.costName || l.materialName));
  lines.forEach(l => {
    const name = l.costName || l.materialName;
    if(existingNameSet.has(name)) return; // 跳过重复（基于名称）
    // 赋予临时序号，稍后 reseq 统一
    l.seq = 0;
    // 确保 classId 被保留
    if(!l.classId && l.materialCategory) {
      try {
        l.classId = parseInt(l.materialCategory);
      } catch(e) {
        // 无法转换则保持原值
      }
    }
    bomWorking.value.push(l);
  });
  reseq();
}
// 查看属性：编辑模式下从 BomSimpleEntryDialog 发出的 request-attr-view，复用 AttributeViewer
const bomAttrViewerRef = ref<InstanceType<typeof AttributeViewer> | null>(null);
function onBomAttrView(payload:any){
  if(!payload) return;
  const defs = (payload.definitions || []).map((d:any)=> ({ ...d }));
  const values = { ...(payload.values || {}) };
  bomAttrViewerRef.value?.open({ id: payload.id || '', attributeDefinitions: defs, attributeValues: values, categoryCode: payload.categoryCode });
}
const dynAttrDialogRef = ref<InstanceType<typeof DynamicAttributeDialog> | null>(null);
async function onBomAttrEdit(payload:any){
  // 打开属性编辑弹窗，并回填结果使名称更新
  if(!payload) return;
  try {
    const result = await dynAttrDialogRef.value?.open({
      categoryCode: payload.categoryCode,
      categoryName: payload.materialName || payload.categoryCode,
      materialName: payload.materialName || '',
      definitions: payload.definitions || [],
      values: payload.values || {}
    } as any);
    if(!result) return; // 用户取消
    bomSimpleDialogRef.value?.updateAttributes(result.values, result.spec);
  } catch(e){ console.error('属性编辑失败', e); }
}
function reseq(){ ['production','packaging'].forEach(t=> { let s=1; bomWorking.value.filter(r=> r.type===t).sort((a,b)=> a.seq-b.seq).forEach(r=> r.seq=s++); }); }
// 停用与删除功能已移除（精简操作列）


// 弹窗引用 & 打开函数
const attrDialogRef = ref<InstanceType<typeof AttributeDialog> | null>(null);
const quotationDialogRef = ref<InstanceType<typeof QuotationDialog> | null>(null);

async function openAttrDialog(row: SkuProductRow) {
  try {
    const detail = await fetchSkuEncodingDetail(row.code);
    // 将返回的字段映射为属性显示（保留已有属性并覆盖）
    const mappedAttrs: Record<string, any> = {
      客户简称: detail.cusShortName || '',
      模具: detail.mould || '',
      尺寸: detail.buildingSize || detail.innerSizeMm || '',
      材质: detail.materialQuality || '',
      功能: detail.productFun || '',
      形状: detail.buildingFinishedShape || '',
      类别: detail.productCategories || ''
    };
    const prod = skuStore.getProduct(row.code);
    if(prod){
      // 同步更新产品名称为后端最新 materialName（若存在）
      if(detail.materialName && typeof detail.materialName === 'string' && detail.materialName.trim()){ prod.name = detail.materialName.trim(); }
      prod.attributes = { ...prod.attributes, ...mappedAttrs };
    }
  } catch(e:any){ console.warn('获取产品属性详情失败', e.message); }
  attrDialogRef.value?.open(row);
}
const bomDialogRef = ref<InstanceType<typeof BomViewDialog> | null>(null);
function openBomDialog(row: SkuProductRow) {
  const items = skuStore.getBom(row.code) || [];
  const displayName = row.name || row.code;
  bomDialogRef.value?.open({ title: 'BOM 明细 - ' + displayName, bomItems: items, group: true });
}
function openQuotationDialog(row: SkuProductRow) { quotationDialogRef.value?.open(row); }

function formatDate(ts: number) { const d = new Date(ts); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; }
// 数量显示（去除重复列后统一）
function quantityDisplay(r:any){
  if(r.feeDetail && r.feeDetail.mode==='count') return r.feeDetail.quantity ?? r.qty;
  return r.qty;
}
function ensureQty(r:any){
  if(!(r.feeDetail && r.feeDetail.mode==='count')) return;
  let q = Number(r.feeDetail.quantity);
  if(!q || q < 1) q = 1;
  r.feeDetail.quantity = Math.round(q);
}
function onDialogNativeClose(){
  // 处理 close 事件（dialog 真正关闭时）
  deletingLine.value = null;
  completeConfirming.value = false;
  dialogOpen.value = false;
}
function onDialogKeydown(e: KeyboardEvent){
  // 全局键盘事件处理，用于 dialog 内的特殊键
  if(e.key === 'Escape'){
    // 如果存在嵌套子 dialog 仍处于 open 状态（例如 BomSimpleEntryDialog），则不处理，交由子层关闭
    const selfEl = dialogRef.value;
    if(selfEl){
      const openChild = Array.from(selfEl.querySelectorAll('dialog[open]')).find(d => d !== selfEl);
      if(openChild){
        // 不阻止默认，不关闭父层，让子层自己处理；直接返回
        return;
      }
    }
    e.preventDefault();
    e.stopPropagation();
    if(deletingLine.value){
      cancelDelete();
      return;
    }
    if(completeConfirming.value){
      cancelCompleteConfirm();
      return;
    }
    // 仅当没有弹层时，按 ESC 才关闭整个弹窗
    closeDialog();
  }
}
function onFormEscapeKey(e: KeyboardEvent){
  // 备用处理器（如果在 form 上捕获到 ESC）
  e.preventDefault();
  e.stopPropagation();
  onDialogKeydown(e);
}
function onDialogCancel(e: Event){
  // 阻止 dialog 默认的 cancel 行为
  if(e instanceof Event){
    e.preventDefault();
    e.stopPropagation();
  }
}
</script>

<style scoped>
.sku-product-detail-page { display:flex; flex-direction:column; gap:1.2rem; animation: pageSlideIn .6s cubic-bezier(.16,1,.3,1); }
@keyframes pageSlideIn { from {opacity:0; transform: translateY(20px);} to {opacity:1; transform:translateY(0);} }
/* 复用与 BOM 简化页面接近的结构化风格 */
.page-header { display:flex; flex-wrap:wrap; justify-content:space-between; align-items:flex-end; gap:1rem; padding:1rem 1.25rem 1.1rem; background:linear-gradient(145deg,#ffffff,#f8fafc); border:1px solid #e2e8f0; border-radius:16px; box-shadow:0 4px 20px -4px rgba(15,23,42,.15),0 2px 8px -2px rgba(15,23,42,.1); position:relative; overflow:hidden; }
.page-header:before { content:''; position:absolute; top:0; left:0; right:0; height:3px; background:linear-gradient(90deg,#3b82f6,#8b5cf6,#06b6d4); opacity:.8; }
.title-wrap { display:flex; flex-direction:column; gap:.4rem; }
.title-wrap h2 { margin:0; font-size:24px; font-weight:700; letter-spacing:-.025em; color:#0f172a; background:linear-gradient(135deg,#1e293b,#475569); background-clip:text; -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
.title-wrap .sub { font-size:13px; color:#64748b; letter-spacing:.01em; font-weight:500; }
.actions { display:flex; gap:.75rem; align-items:center; flex-wrap:wrap; }
.search-box { display:flex; align-items:center; gap:.5rem; padding:.6rem .8rem; border:1px solid #cbd5e1; border-radius:12px; background:#fff; min-width:260px; transition:all .3s cubic-bezier(.4,0,.2,1); position:relative; }
.search-box:focus-within { border-color:#3b82f6; box-shadow:0 0 0 4px rgba(59,130,246,.15),0 4px 12px -2px rgba(59,130,246,.2); transform:translateY(-1px); }
.search-icon { width:18px; height:18px; color:#6b7280; transition:color .3s; }
.search-box:focus-within .search-icon { color:#3b82f6; }
.search-box input { border:none; outline:none; font-size:14px; flex:1; font-weight:500; color:#374151; }
.search-box input::placeholder { color:#9ca3af; font-weight:400; }
.search-box .search-hint { position:absolute; right:.65rem; top:50%; transform:translateY(-50%); font-size:11px; color:#64748b; pointer-events:none; }
.select { padding:.6rem .8rem; border:1px solid #cbd5e1; border-radius:12px; background:#fff; font-size:14px; min-width:160px; transition:all .3s cubic-bezier(.4,0,.2,1); font-weight:500; }
.select:focus { border-color:#3b82f6; box-shadow:0 0 0 4px rgba(59,130,246,.15); transform:translateY(-1px); }
.btn { position:relative; cursor:pointer; border:1px solid transparent; padding:.65rem 1.2rem; border-radius:12px; font-size:14px; font-weight:600; letter-spacing:.025em; line-height:1; display:inline-flex; align-items:center; gap:.5rem; transition:all .3s cubic-bezier(.4,0,.2,1); overflow:hidden; }
.btn.no-wrap { white-space: nowrap; }
.btn.primary { background:linear-gradient(135deg,#3b82f6,#2563eb); color:#fff; border-color:#2563eb; }
.btn.primary:hover { background:linear-gradient(135deg,#2563eb,#1d4ed8); transform:translateY(-2px); }
.btn.primary:active { transform:translateY(1px) scale(.98); }
.btn.ghost { background:rgba(255,255,255,.8); border:1px solid #e2e8f0; color:#374151; }
.btn.ghost:hover { background:#f8fafc; border-color:#cbd5e1; transform:translateY(-1px); }
.btn.ghost.danger { color:#dc2626; }
.btn.ghost.danger:hover { background:#fef2f2; border-color:#fecaca; }
.btn.ghost.blue { color:#2563eb; }
.btn.ghost.blue:hover { background:#eff6ff; border-color:#bfdbfe; }
.btn.sm { padding:.5rem .8rem; font-size:13px; border-radius:10px; }
.btn.view { position:relative; background:linear-gradient(135deg,#ffffff,#f8fafc); border:1px solid #e2e8f0; color:#374151; padding:.5rem .9rem; border-radius:20px; overflow:hidden; }
.btn.view .txt { font-size:12px; font-weight:600; letter-spacing:.025em; }
.btn.view:hover { border-color:#3b82f6; color:#1e40af; box-shadow:0 4px 12px -2px rgba(59,130,246,.3); }
.table-wrapper.card { border:1px solid #e2e8f0; border-radius:16px; background:#fff; box-shadow:0 8px 25px -6px rgba(15,23,42,.1),0 3px 10px -2px rgba(15,23,42,.05); padding:.5rem .8rem .9rem; overflow:hidden; animation: tableSlideIn .8s cubic-bezier(.16,1,.3,1) .2s both; }
@keyframes tableSlideIn { from {opacity:0; transform:translateY(30px);} to {opacity:1; transform:translateY(0);} }
.entry-table { width:100%; border-collapse:separate; border-spacing:0; font-size:14px; }
.entry-table.fancy thead th { background:linear-gradient(135deg,#f8fafc,#f1f5f9); font-weight:700; font-size:12px; letter-spacing:.05em; text-transform:uppercase; color:#475569; position:sticky; top:0; z-index:1; border-bottom:2px solid #e2e8f0; }
.entry-table th,.entry-table td { padding:.8rem .9rem; text-align:left; }
.entry-table tbody tr { border-radius:8px; transition:all .3s cubic-bezier(.4,0,.2,1); }
.entry-table tbody tr:hover { background:linear-gradient(135deg,#f8fafc,#f1f5f9); box-shadow:0 4px 12px -2px rgba(59,130,246,.15); }
.ops { display:flex; gap:.5rem; }
.empty-tip { color:#64748b; text-align:center; padding:3rem 0; font-size:16px; font-weight:500; }
.pagination-bar { display:flex; gap:.8rem; align-items:center; justify-content:flex-end; padding:.4rem .4rem 0; }
.pagination-bar .pager-info { font-size:12px; color:#475569; }
.status-badge { display:inline-block; padding:.35rem .65rem; border-radius:20px; font-size:12px; font-weight:600; letter-spacing:.03em; background:#e2e8f0; color:#334155; }
.status-badge.pending { background:linear-gradient(135deg,#fde68a,#fcd34d); color:#92400e; }
.status-badge.completed { background:linear-gradient(135deg,#dcfce7,#bbf7d0); color:#166534; }
/* 完成按钮样式 */
.btn.success { background:linear-gradient(135deg,#16a34a,#22c55e); color:#fff; border-color:#16a34a; }
.btn.success:hover { background:linear-gradient(135deg,#15803d,#16a34a); transform:translateY(-2px); }
.btn.success:active { transform:translateY(1px) scale(.97); }
.btn[disabled] { opacity:.55; cursor:not-allowed; }
/* dialog （复用已有通用类，可精简） */
.entry-dialog::backdrop { background:rgba(15,23,42,.55); }
.entry-dialog { border:none; padding:0; border-radius:20px; width:640px; max-width:95vw; box-shadow:0 12px 32px -8px rgba(15,23,42,.3),0 4px 18px -6px rgba(15,23,42,.18); overflow:hidden; }
.entry-dialog.closing { opacity:0; transform:translateY(-8px) scale(.96); filter:blur(2px); pointer-events:none; transition:opacity .18s cubic-bezier(.4,0,.2,1), transform .18s cubic-bezier(.4,0,.2,1), filter .18s cubic-bezier(.4,0,.2,1); }
.entry-dialog.closing::backdrop { background:rgba(15,23,42,0); backdrop-filter:blur(0); }
.dialog-shell { display:flex; flex-direction:column; width:100%; max-height:calc(100dvh - 6rem); background:linear-gradient(145deg,#ffffff,#f8fafc 60%,#f1f5f9); }
.dialog-header { flex-shrink:0; display:flex; justify-content:space-between; align-items:center; padding:1.05rem 1.5rem .85rem; border-bottom:1px solid #e2e8f0; background:linear-gradient(135deg,#ffffff,#f8fafc); }
.dialog-title { margin:0; font-size:20px; font-weight:700; }
.dialog-content-scroll { padding:1.1rem 1.6rem 1.4rem; overflow-y:auto; display:flex; flex-direction:column; gap:1.2rem; }
.form-section-card { border:1px solid #e2e8f0; background:linear-gradient(135deg,#ffffff,#f8fafc); border-radius:18px; padding:1.05rem 1.15rem 1.2rem; box-shadow:0 6px 18px -6px rgba(15,23,42,.15),0 2px 6px -2px rgba(15,23,42,.06); display:flex; flex-direction:column; gap:1rem; }
.section-title-row { display:flex; flex-wrap:wrap; align-items:center; gap:.8rem; }
.section-title { margin:0; font-size:15px; font-weight:700; letter-spacing:.02em; position:relative; padding-left:.85rem; }
.section-title:before { content:""; position:absolute; left:0; top:50%; transform:translateY(-50%); width:4px; height:70%; background:linear-gradient(135deg,#3b82f6,#8b5cf6); border-radius:2px; }
.grid-two { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:1rem 1.2rem; }
.field-block { display:flex; flex-direction:column; gap:.45rem; font-size:13px; font-weight:600; color:#374151; }
.field-block input,.field-block select { padding:.75rem .9rem; border:1px solid #d1d5db; border-radius:12px; font-size:14px; background:#fff; transition:all .25s cubic-bezier(.4,0,.2,1); font-weight:500; }
.field-block input:focus,.field-block select:focus { border-color:#3b82f6; box-shadow:0 0 0 4px rgba(59,130,246,.12); outline:none; transform:translateY(-1px); }
.field-block input.readonly,.field-block select.readonly { background:#f1f5f9; cursor:not-allowed; opacity:.75; }
.field-block .multiline-name { padding:.55rem .75rem .6rem; border:1px solid #d1d5db; border-radius:12px; background:#f1f5f9; font-size:13px; font-weight:600; line-height:1.4; max-height:120px; overflow:auto; white-space:normal; word-break:break-word; letter-spacing:.02em; }
.field-block .multiline-name::-webkit-scrollbar { width:8px; }
.field-block .multiline-name::-webkit-scrollbar-track { background:transparent; }
.field-block .multiline-name::-webkit-scrollbar-thumb { background:#cbd5e1; border-radius:8px; }
.field-block .multiline-name:hover { box-shadow:0 0 0 3px rgba(59,130,246,.15); }
.fee-input { max-width:120px; padding:.55rem .6rem; font-size:13px; }
.fee-total-line { margin-top:.6rem; padding:.65rem .8rem; background:linear-gradient(135deg,#f8fafc,#f1f5f9); border-radius:10px; font-size:13px; color:#374151; text-align:right; border:1px solid #e2e8f0; }
.fee-total-line strong { color:#1e40af; font-weight:700; font-size:14px; }
.grid-three { display:flex; gap:1.2rem; flex-wrap:wrap; }
.sticky-actions { position:sticky; bottom:0; background:linear-gradient(180deg,rgba(255,255,255,.95),rgba(255,255,255,.98) 70%); display:flex; justify-content:flex-end; align-items:center; gap:1rem; padding:.7rem 1.4rem .85rem; border-top:1px solid #e2e8f0; }
.actions-btns { display:flex; gap:.85rem; }
.btn.secondary { background:#ffffff; border:1px solid #d1d5db; color:#374151; }
.btn.secondary:hover { background:#f8fafc; border-color:#cbd5e1; transform:translateY(-2px); }
.btn.primary[disabled] { opacity:.55; cursor:not-allowed; transform:none; box-shadow:none; }
.close-x { background:transparent; border:none; cursor:pointer; padding:.5rem; border-radius:10px; color:#6b7280; transition:all .2s; display:flex; align-items:center; justify-content:center; }
.close-x:hover { background:linear-gradient(135deg,#fee2e2,#fecaca); color:#dc2626; transform:rotate(90deg); }
.close-icon { width:20px; height:20px; }
/* BOM table 样式 - entry-table.fancy */
.bom-table-wrapper { margin-top: 12px; border-radius: 16px; overflow: auto; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); display: flex; flex-direction: column; max-height: 500px; scrollbar-gutter: stable; }
.bom-table-wrapper::-webkit-scrollbar { width: 10px; }
.bom-table-wrapper::-webkit-scrollbar-track { background: transparent; border-radius: 8px; }
.bom-table-wrapper::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 8px; transition: background 0.2s; }
.bom-table-wrapper::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
.entry-table { width: 100%; border-collapse: collapse; font-size: 13px; background-color: #fff; }
.entry-table thead { position: sticky; top: 0; z-index: 40; }
.entry-table.fancy th { background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); color: #1f2937; font-weight: 700; padding: 12px 8px; text-align: left; border-bottom: 2px solid #e5e7eb; white-space: nowrap; text-transform: uppercase; font-size: 12px; letter-spacing: 0.5px; position: relative; z-index: 41; }
.entry-table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; text-align: left; height: 44px; vertical-align: middle; }
.entry-table.fancy tbody tr { transition: all 0.2s ease; }
.entry-table td.strong { font-weight: 600; color: #1f2937; }
.entry-table td.type-cell { font-weight: 600; color: #1f2937; width: 60px; }
.entry-table td.category-cell { font-weight: 500; color: #6b7280; background-color: #f3f4f6; border-radius: 4px; text-align: center; }
.entry-table td.spec-cell { max-width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.entry-table td.fee-mode { color: #2563eb; font-weight: 500; }
.entry-table td.fee-amount { color: #059669; font-weight: 500; text-align: right; }
.entry-table td.qty { text-align: center; }
.entry-table td.line-cost { color: #1e40af; font-weight: 600; text-align: right; }
.entry-table td.actions { display: flex; justify-content: flex-start; gap: 8px; align-items: center; white-space: nowrap; width: 100px; height: 44px; }
.entry-table .qty-input { width: 50px; padding: 6px; border: 1px solid #d1d5db; border-radius: 4px; text-align: center; font-size: 13px; }
.entry-table .qty-input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }
.entry-table td.empty { text-align: center; color: #9ca3af; padding: 32px 8px; border: none; }
.entry-table tfoot { background-color: #fafbfc; border-top: 2px solid #e5e7eb; }
.entry-table tfoot td { font-weight: 600; color: #1f2937; padding: 12px 8px; border: none; }
.entry-table .link-btn { background: none; border: none; color: #3b82f6; cursor: pointer; padding: 0; font-size: 12px; text-decoration: underline; transition: color 0.2s; }
.entry-table .link-btn:hover { color: #1d4ed8; }
.entry-table .link-btn.danger { color: #ef4444; }
.entry-table .link-btn.danger:hover { color: #dc2626; }
.entry-table .tar { text-align: right; }

/* inline tools 样式优化 */
.bom-head-inline .inline-tools { display:flex; gap:.6rem; align-items:center; flex-wrap:wrap; }
.bom-head-inline .filter-inline { padding:.55rem .8rem .55rem 2.1rem; border:1px solid #d1d5db; border-radius:10px; font-size:13px; background:#fff url('data:image/svg+xml;utf8,<svg fill="none" stroke="%23647589" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.35-4.35"/></svg>') .6rem center/16px 16px no-repeat; transition:all .25s; min-width:230px; }
.bom-head-inline .filter-inline:focus { outline:none; border-color:#3b82f6; box-shadow:0 0 0 3px rgba(59,130,246,.2); }
.bom-head-inline .btn.secondary.sm { position:relative; padding-left:2rem; }
.bom-head-inline .btn.secondary.sm:before { content:""; position:absolute; left:.9rem; top:50%; transform:translateY(-50%); width:14px; height:14px; background:url('data:image/svg+xml;utf8,<svg viewBox="0 0 24 24" fill="none" stroke="%232563eb" stroke-width="2" xmlns="http://www.w3.org/2000/svg"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>') center/14px 14px no-repeat; opacity:.85; }
.bom-head-inline .btn.secondary.sm:hover:before { filter:brightness(.9); }

/* 行内操作 链接按钮 */
.link-btn { background:none; border:none; padding:0; cursor:pointer; font-size:12px; font-weight:600; color:#2563eb; position:relative; }
.link-btn:hover { color:#1d4ed8; text-decoration:underline; }
.link-btn.danger { color:#dc2626; }
.link-btn.danger:hover { color:#b91c1c; }

/* 删除确认 overlay */
.confirm-center-layer { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; background:rgba(15,23,42,.40); z-index:50; padding:1.5rem; }
.confirm-card { display:flex; flex-direction:column; gap:.65rem; padding:1rem 1.15rem 1.15rem; width:360px; max-width:100%; background:linear-gradient(145deg,#ffffff,#f8fafc); border:1px solid #e2e8f0; border-radius:20px; box-shadow:0 18px 40px -12px rgba(15,23,42,.35),0 6px 20px -8px rgba(15,23,42,.25); animation:popIn .38s cubic-bezier(.16,1,.3,1); }
@keyframes popIn { from {opacity:0; transform:translateY(6px) scale(.95);} to {opacity:1; transform:translateY(0) scale(1);} }
.confirm-head { display:flex; gap:.55rem; align-items:center; }
.confirm-head h5 { margin:0; font-size:15px; font-weight:700; letter-spacing:.02em; }
.warn-icon { width:20px; height:20px; color:#dc2626; }
.confirm-text { margin:0; font-size:13px; line-height:1.45; color:#475569; }
.confirm-text strong { color:#0f172a; }
.confirm-actions { display:flex; justify-content:flex-end; gap:.5rem; }

/* 过渡 */
.fade-scale-enter-active, .fade-scale-leave-active { transition:all .28s cubic-bezier(.4,0,.2,1); }
.fade-scale-enter-from, .fade-scale-leave-to { opacity:0; transform:translateY(4px) scale(.96); }

/* 宽屏优化: 放大编辑弹窗并采用双栏布局（左侧基础/费用，右侧大区域为 BOM） */
@media (min-width: 1100px) {
  .entry-dialog.large { width:1080px; }
  /* 两列：左侧堆叠基础与非物料，右侧为 BOM；区域高度对齐，表格内容自适应 */
  .dialog-content-scroll.big-grid { display:grid; grid-template-columns:260px 1fr; gap:1.1rem 1.3rem; align-items:stretch; }
  .dialog-content-scroll.big-grid .left-stack { display:flex; flex-direction:column; gap:1.2rem; }
  .dialog-content-scroll.big-grid .right-stack { display:flex; flex-direction:column; gap:1rem; }
  /* 非物料成本输入行改为纵向单列排布，避免横向空白 */
  .dialog-content-scroll.big-grid .extra-fees-edit .grid-three { flex-direction:column; flex-wrap:nowrap; gap:.85rem; }
  .dialog-content-scroll.big-grid .extra-fees-edit .grid-three .field-block { width:100%; }
  /* BOM 区域框与非物料成本区域框下边界对齐 */
  .dialog-content-scroll.big-grid .bom-edit { display:flex; flex-direction:column; flex:1; min-height:0; }
  .dialog-content-scroll.big-grid .bom-edit .bom-table-wrapper { display:flex; flex-direction:column; overflow-y:auto; overflow-x:hidden; min-height:0; max-height: 500px; scrollbar-gutter: stable; }
  .dialog-content-scroll.big-grid .bom-edit .bom-table-wrapper::-webkit-scrollbar { width:10px; }
  .dialog-content-scroll.big-grid .bom-edit .bom-table-wrapper::-webkit-scrollbar-track { background:transparent; }
  .dialog-content-scroll.big-grid .bom-edit .bom-table-wrapper::-webkit-scrollbar-thumb { background:#cbd5e1; border-radius:6px; }
  .dialog-content-scroll.big-grid .bom-edit .bom-table-wrapper::-webkit-scrollbar-thumb:hover { background:#94a3b8; }
  .dialog-content-scroll.big-grid .bom-edit .bom-table-wrapper .entry-table { width:100%; }
}
@media (min-width: 1500px) {
  .entry-dialog.large { width:1280px; }
  .dialog-content-scroll.big-grid { grid-template-columns:280px 1fr; }
  .dialog-content-scroll.big-grid .extra-fees-edit .grid-three { flex-direction:column; }
}

/* 小屏仍保持单列，不破坏已有滚动 */
</style>
